package com.ease.map.layer {
    import com.ease.map.common.basetype.linkedlist.ILinkedListNode;
    import com.ease.map.common.basetype.linkedlist.LinkedList;
    import com.ease.map.common.basetype.linkedlist.LinkedListBitmapNode;
    import com.ease.map.events.MapEvent;
    import com.ease.map.events.TileEvent;
    import com.ease.map.geometry.basetype.Extent;
    import com.ease.map.geometry.basetype.Location;
    import com.ease.map.geometry.basetype.Pixel;
    import com.ease.map.geometry.basetype.Size;
    import com.ease.map.layer.params.IHttpParams;
    import com.ease.map.tile.ImageTile;
    import flash.display.Bitmap;
    import com.ease.map.layer.Layer;

    /**
     * 瓦片基础图层
     */
    public class Grid extends Layer {
        private const DEFAULT_TILE_WIDTH:Number = 256;

        private const DEFAULT_TILE_HEIGHT:Number = 256;

        /** imagetile列表**/
        private var _grid:Vector.<Vector.<ImageTile>> = null;

        private var _singleTile:Boolean = false;

        private var _numLoadingTiles:int = 0;

        private var _origin:Pixel = null;

        private var _xBuffer:int = 0;
        private var _yBuffer:int = 0;

        protected var CACHE_SIZE:int = 64;

        private var tileCache:LinkedList = new LinkedList();
        private var cptCached:int = 0;

        private var _tileWidth:Number = DEFAULT_TILE_WIDTH;
        private var _tileHeight:Number = DEFAULT_TILE_HEIGHT;

        private var _url:String = null;
        private var _method:String = null;
        private var _params:IHttpParams = null;

        /**
         * Create a new grid layer
         *
         * @param name
         * @param url
         * @param params
         */
        public function Grid(id:int, name:String, url:String, params:IHttpParams = null) {
            super(id, name);
            this._url = url;
            this.params = params;

            this.addEventListener(TileEvent.TILE_LOAD_END, tileLoadHandler);
            this.addEventListener(TileEvent.TILE_LOAD_START, tileLoadHandler);
        }

        public function get params():IHttpParams {
            return _params;
        }

        public function set params(value:IHttpParams):void {
            _params = value;
        }

        public function get method():String {
            return _method;
        }

        public function set method(value:String):void {
            _method = value;
        }

        public function get url():String {
            return _url;
        }

        public function set url(value:String):void {
            _url = value;
        }

        override protected function onMapMove(e:MapEvent):void {
            // Clear pending requests after zooming in order to avoid to add
            // too many tile requests  when the user is zooming step by step
            if (e.zoomChanged) {
                var j:uint;
                for each (var array:Vector.<ImageTile>in this._grid) {
                    j = array.length;
                    for (var i:Number = 0; i < j; i++) {
                        var tile:ImageTile = array[i];
                        if (tile != null && !tile.loadComplete) {
                            tile.clear();
                        }
                    }
                }
            }
            super.onMapMove(e);
        }

        override public function destroy():void {
            this.clear();
            tileCache.clear();
            this.grid = null;
            super.destroy();
        }

        /**
         * Go through and remove all tiles from the grid, calling
         *    destroy() on each of them to kill circular references
         */
        override public function clear():void {
            if (this._grid && this._grid.length > 0) {
                var i:uint = this._grid.length;
                var j:uint = this._grid[0].length;
                var iRow:uint;
                var iCol:uint;
                for (iRow = 0; iRow < i; ++iRow) {
                    var row:Vector.<ImageTile> = this._grid.pop();
                    for (iCol = 0; iCol < j; ++iCol) {
                        var tile:ImageTile = row.pop();
                        this.removeTileMonitoringHooks(tile);
                        tile.destroy();
                    }
                }
                while (this.numChildren > 0)
                    this.removeChildAt(0);
                this.grid = null;
            }
        }

        /**
         * cache a tile
         */
        public function addTileCache(node:ILinkedListNode):void {
            if (!tileCache.moveTail(node.uid)) {
                tileCache.insertTail(node);
                cptCached++;
                if (cptCached == CACHE_SIZE + 1) {
                    tileCache.removeHead();
                    cptCached--;
                }
            }
        }

        /**
         * get a cached tile
         */
        public function getTileCache(url:String):Bitmap {
            var node:ILinkedListNode = tileCache.getUID(url);
            if (node == null) {
                return null;
            }
            else if (node is LinkedListBitmapNode) {
                this.addTileCache(node);
                return (node as LinkedListBitmapNode).bitmap();
            }
            return null;
        }

        override public function redraw(fullRedraw:Boolean = true):void {
            if (!displayed) {
                this.clear();
                return;
            }

            var bounds:Extent = this.map.extent.clone();

            var forceReTile:Boolean = this._grid == null || !this._grid.length || fullRedraw;

            var tilesBounds:Extent = this.getTilesBounds();

            if (this.singleTile) {
                if (fullRedraw)
                    this.clear();
                if (forceReTile || !tilesBounds.containsBounds(bounds)) {
                    this.initSingleTile(bounds);
                }
            }
            else {
                if (forceReTile || !tilesBounds.containsBounds(bounds, true)) {
                    this.initGriddedTiles(bounds);
                }
                else {
                    this.moveGriddedTiles(bounds);
                }
            }
        }

        public function set tileWidth(value:Number):void {
            this._tileWidth = value;
        }

        public function get tileWidth():Number {
            if (this.singleTile) {
                return map.size.w;
            }
            return this._tileWidth;
        }

        public function set tileHeight(value:Number):void {
            this._tileHeight = value;
        }

        public function get tileHeight():Number {
            if (this.singleTile) {
                return map.size.h;
            }
            return this._tileHeight;
        }


        /**
         * Return the bounds of the tile grid.
         *
         * @return A Bounds object representing the bounds of all the currently loaded tiles
         */
        public function getTilesBounds():Extent {
            if (this._grid == null)
                return null;
            var i:uint = this._grid.length;

            if (i > 0) {
                var bottom:int = i - 1;
                var bottomLeftTile:ImageTile = this._grid[bottom][0];
                var right:int = this._grid[0].length - 1;
                var topRightTile:ImageTile = this._grid[0][right];
                return new Extent(bottomLeftTile.bounds.xmin, bottomLeftTile.bounds.ymin, topRightTile.bounds.xmax, topRightTile.bounds.ymax);
            }
            return null;
        }

        /**
         * Initialization singleTile
         *
         * @param bounds
         */
        public function initSingleTile(bounds:Extent):void {
            var center:Location = bounds.center;
            var tileWidth:Number = bounds.width;
            var tileHeight:Number = bounds.height;
            var tileBounds:Extent = new Extent(center.lon - (tileWidth / 2), center.lat - (tileHeight / 2), center.lon + (tileWidth / 2), center.lat + (tileHeight / 2));
            var ul:Location = new Location(tileBounds.xmin, tileBounds.ymax);
            var px:Pixel = this.map.getLayerPxFromLocation(ul);

            if (this._grid == null) {
                this._grid = new Vector.<Vector.<ImageTile>>(1);
                this._grid[0] = new Vector.<ImageTile>(1);
                this._grid[0][0] = null;
            }

            var tile:ImageTile = this._grid[0][0];
            if (!tile) {
                tile = this.addTile(tileBounds, px);
                tile.draw();
                this._grid[0][0] = tile;
            }
            else {
                tile.moveTo(tileBounds, px);
            }
            this.removeExcessTiles(1, 1);
        }

        /**
         * Ititialize gridded tiles
         *
         * when clearTiles == true (most of time), Tile.clearAndMoveTo is called.
         * This method reset tile, so produce a white flash (usefull when loading map)
         * Actually used when zooming in / out
         *
         * when clearTiles == false, Tile.moveTo is called,
         * no white flash, but there is some problems if used for something else than modifying map extent
         */
        public function initGriddedTiles(bounds:Extent, clearTiles:Boolean = true):void {
            var viewSize:Size = this.map.size;
            var minRows:Number = Math.ceil(viewSize.h / this.tileHeight) + Math.max(1, 2 * this.yBuffer);
            var minCols:Number = Math.ceil(viewSize.w / this.tileWidth) + Math.max(1, 2 * this.xBuffer);
            var extent:Extent = this.map.maxExtent;
            var resolution:Number = this.map.resolution;
            var tilelon:Number = resolution * this.tileWidth;
            var tilelat:Number = resolution * this.tileHeight;
            var offsetlon:Number = bounds.xmin - extent.xmin;
            var tilecol:Number = Math.floor(offsetlon / tilelon) - this.xBuffer;
            var tilecolremain:Number = offsetlon / tilelon - tilecol;
            var tileoffsetx:Number = -tilecolremain * this.tileWidth;
            var tileoffsetlon:Number = extent.xmin + tilecol * tilelon;
            var offsetlat:Number = bounds.ymax - (extent.ymin + tilelat);
            var tilerow:Number = Math.ceil(offsetlat / tilelat) + this.yBuffer;
            var tilerowremain:Number = tilerow - offsetlat / tilelat;
            var tileoffsety:Number = -tilerowremain * this.tileHeight;
            var tileoffsetlat:Number = extent.ymin + tilerow * tilelat;

            tileoffsetx = Math.round(tileoffsetx);
            tileoffsety = Math.round(tileoffsety);

            this._origin = new Pixel(tileoffsetx, tileoffsety);

            var startX:Number = tileoffsetx;
            var startLon:Number = tileoffsetlon;
            var rowidx:int = 0;

            if (this._grid == null) {
                this._grid = new Vector.<Vector.<ImageTile>>();
            }
            do {
                var row:Vector.<ImageTile>;
                if (this._grid.length == rowidx) {
                    row = new Vector.<ImageTile>;
                    this._grid.push(row);
                }
                else {
                    row = this._grid[rowidx];
                }
                rowidx = ++rowidx;

                tileoffsetlon = startLon;
                tileoffsetx = startX;
                var colidx:int = 0;
                do {
                    var tileBounds:Extent = new Extent(tileoffsetlon, tileoffsetlat, tileoffsetlon + tilelon, tileoffsetlat + tilelat);
                    var x:Number = tileoffsetx;
                    x -= int(this.map.layerContainer.x);

                    var y:Number = tileoffsety;
                    y -= int(this.map.layerContainer.y);

                    var px:Pixel = new Pixel(x, y);
                    var tile:ImageTile;
                    if (row.length == colidx) {
                        tile = this.addTile(tileBounds, px);
                        row.push(tile);
                    }
                    else {
                        tile = row[colidx];
                        if (clearTiles)
                            tile.clearAndMoveTo(tileBounds, px, false);
                        else
                            tile.moveTo(tileBounds, px, false);
                    }
                    colidx = ++colidx;

                    tileoffsetlon += tilelon;
                    tileoffsetx += this.tileWidth;
                } while ((tileoffsetlon <= bounds.xmax + tilelon * this.xBuffer) || colidx < minCols)

                tileoffsetlat -= tilelat;
                tileoffsety += this.tileHeight;
            } while ((tileoffsetlat >= bounds.ymin - tilelat * this.yBuffer) || rowidx < minRows)

            //shave off exceess rows and colums
            this.removeExcessTiles(rowidx, colidx);

            //now actually draw the tiles
            this.spiralTileLoad();
        }

        /**
         *   右上角开始顺时针由外到内 adding tiles加入队列
         *   加载完毕后，逆时针draw队列里每个tile.
         */
        protected function spiralTileLoad():void {
            var tileQueue:Array = new Array();
            var directions:Array = ["right", "down", "left", "up"];
            var iRow:int = 0;
            var iCell:int = -1;
            var direction:int = 0;
            var directionsTried:int = 0;

            while (directionsTried < 4) {
                var testRow:int = iRow;
                var testCell:int = iCell;
                switch (directions[direction]) {
                    case "right":
                        testCell++;
                        break;
                    case "down":
                        testRow++;
                        break;
                    case "left":
                        testCell--;
                        break;
                    case "up":
                        testRow--;
                        break;
                }

                // if the test grid coordinates are within the bounds of the
                //  grid, get a reference to the tile.
                var tile:ImageTile = null;
                if ((testRow < this._grid.length) && (testRow >= 0) && (testCell < this._grid[testRow].length) && (testCell >= 0)) {
                    tile = this._grid[testRow][testCell];
                }

                if ((tile != null) && (!tile.queued)) {
                    //add tile to beginning of queue, mark it as queued.
                    tileQueue.push(tile);
                    tile.queued = true;

                    //restart the directions counter and take on the new coords
                    directionsTried = 0;
                    iRow = testRow;
                    iCell = testCell;
                }
                else {
                    //need to try to load a tile in a different direction
                    direction = (direction + 1) % 4;
                    directionsTried++;
                }
            }

            // now we go through and draw the tiles in forward order
            for (var i:int = tileQueue.length - 1; i >= 0; i--) {
                tile = tileQueue[i];
                tile.draw();
                //mark tile as unqueued for the next time (since tiles are reused)
                tile.queued = false;
            }
        }

        public function addTile(bounds:Extent, position:Pixel):ImageTile {
            return null;
        }


        public function removeTileMonitoringHooks(tile:ImageTile):void {
        }

        /**
         * This metod is called only when mapEvent.MOVE_END is thrown
         */
        public function moveGriddedTiles(bounds:Extent):void {
            var xbuffer:Number = this.xBuffer || 1;
            var ybuffer:Number = this.yBuffer || 1;
            while (true) {
                var tlLayer:Pixel = this.grid[0][0].position;
                var tlViewPort:Pixel = this.map.getMapPxFromLayerPx(tlLayer);
                if (tlViewPort.x > -this.tileWidth * (xbuffer - 1)) {
                    this.shiftColumn(true);
                }
                else if (tlViewPort.x < -this.tileWidth * xbuffer) {
                    this.shiftColumn(false);
                }
                else if (tlViewPort.y > -this.tileHeight * (ybuffer - 1)) {
                    this.shiftRow(true);
                }
                else if (tlViewPort.y < -this.tileHeight * ybuffer) {
                    this.shiftRow(false);
                }
                else {
                    break;
                }
            };
            if (this.xBuffer == 0 && this.yBuffer == 0) {
                var rl:int = this._grid.length;
                var cl:int;
                for (var r:int = 0; r < rl; r++) {
                    var row:Vector.<ImageTile> = this._grid[r];
                    cl = row.length;
                    for (var c:int = 0; c < cl; ++c) {
                        var tile:ImageTile = row[c];
                        if (!tile.drawn && tile.bounds.intersectsBounds(bounds, false)) {
                            tile.draw();
                        }
                    }
                }
            }
        }

        /**
         * Shifty grid work
         *
         * @param prepend if true, prepend to beginning.
         *                          if false, then append to end
         */
        private function shiftRow(prepend:Boolean):void {
            var modelRowIndex:int = (prepend) ? 0 : (this._grid.length - 1);
            var modelRow:Vector.<ImageTile> = this._grid[modelRowIndex];
            var resolution:Number = this.map.resolution;
            var deltaY:Number = (prepend) ? -this.tileHeight : this.tileHeight;
            var deltaLat:Number = resolution * -deltaY;
            var row:Vector.<ImageTile> = (prepend) ? this._grid.pop() : this._grid.shift();

            var j:uint = modelRow.length;
            for (var i:uint = 0; i < j; ++i) {
                var modelTile:ImageTile = modelRow[i];
                var bounds:Extent = modelTile.bounds.clone();
                var position:Pixel = modelTile.position.clone();
                bounds.ymin = bounds.ymin + deltaLat;
                bounds.ymax = bounds.ymax + deltaLat;
                position.y = position.y + deltaY;
                row[i].clearAndMoveTo(bounds, position);
            }

            if (prepend) {
                this._grid.unshift(row);
            }
            else {
                this._grid.push(row);
            }
        }

        /**
         * Shift grid work in the other dimension
         *
         * @param prepend if true, prepend to beginning.
         *                          if false, then append to end
         */
        private function shiftColumn(prepend:Boolean):void {
            var deltaX:Number = (prepend) ? -this.tileWidth : this.tileWidth;
            var resolution:Number = this.map.resolution;
            var deltaLon:Number = resolution * deltaX;

            var j:uint = this._grid.length;
            for (var i:uint = 0; i < j; ++i) {
                var row:Vector.<ImageTile> = this._grid[i];
                var modelTileIndex:int = (prepend) ? 0 : (row.length - 1);
                var modelTile:ImageTile = row[modelTileIndex];
                var bounds:Extent = modelTile.bounds.clone();
                var position:Pixel = modelTile.position.clone();
                bounds.xmin = bounds.xmin + deltaLon;
                bounds.xmax = bounds.xmax + deltaLon;
                position.x = position.x + deltaX;
                var tile:ImageTile = prepend ? this._grid[i].pop() : this._grid[i].shift()
                tile.clearAndMoveTo(bounds, position);
                if (prepend) {
                    this._grid[i].unshift(tile);
                }
                else {
                    this._grid[i].push(tile);
                }
            }
        }

        /**
         * 当buffer改变时，需要移除一些超出的行列
         *
         * @param rows Maximum number of rows we want our grid to have.
         * @param colums Maximum number of columns we want our grid to have.
         */
        public function removeExcessTiles(rows:int, columns:int):void {
            while (this._grid.length > rows) {
                var row:Vector.<ImageTile> = this._grid.pop();
                for (var i:int = 0, l:int = row.length; i < l; i++) {
                    var tile:ImageTile = row[i];
                    this.removeTileMonitoringHooks(tile)
                    tile.destroy();
                }
            }

            while (this._grid[0].length > columns) {
                for (i = 0, l = this._grid.length; i < l; i++) {
                    row = this._grid[i];
                    tile = row.pop();
                    this.removeTileMonitoringHooks(tile);
                    tile.destroy();
                }
            }
        }

        /**
         * Returns The tile bounds for a layer given a pixel location.
         *
         * @param viewPortPx The location in the viewport.
         *
         * @return Bounds of the tile at the given pixel location.
         */
        public function getTileBounds(viewPortPx:Pixel):Extent {
            var maxExtent:Extent = this.map.maxExtent;
            var resolution:Number = this.map.resolution;
            var tileMapWidth:Number = resolution * this.tileWidth;
            var tileMapHeight:Number = resolution * this.tileHeight;
            var mapPoint:Location = this.map.getLocationFromMapPx(viewPortPx);
            var tileLeft:Number = maxExtent.xmin + (tileMapWidth * Math.floor((mapPoint.lon - maxExtent.xmin) / tileMapWidth));
            var tileBottom:Number = maxExtent.ymin + (tileMapHeight * Math.floor((mapPoint.lat - maxExtent.ymin) / tileMapHeight));
            return new Extent(tileLeft, tileBottom, tileLeft + tileMapWidth, tileBottom + tileMapHeight, this.projection);
        }

        private function tileLoadHandler(event:TileEvent):void {
            switch (event.type) {
                case TileEvent.TILE_LOAD_START:  {
                    // set layer loading to true
                    this.loading = true;
                    break;
                }
                case TileEvent.TILE_LOAD_END:  {
                    // check if there are still tiles loading
                    for each (var array:Vector.<ImageTile>in this._grid) {
                        var j:uint = array.length;
                        for (var i:Number = 0; i < j; i++) {
                            var tile:ImageTile = array[i];
                            if (tile != null && !tile.loadComplete)
                                return;
                        }
                    }
                    this.loading = false;
                    break;
                }
            }
        }

        //Getters and Setters
        override public function get imageSize():Size {
            if (this._imageSize == null)
                this._imageSize = new Size(this.tileWidth, this.tileHeight);
            return new Size(this.tileWidth, this.tileHeight);
        }

        public function get grid():Vector.<Vector.<ImageTile>> {
            return this._grid;
        }

        public function set grid(value:Vector.<Vector.<ImageTile>>):void {
            this._grid = value;
        }

        public function get singleTile():Boolean {
            return this._singleTile;
        }

        public function set singleTile(value:Boolean):void {
            this._singleTile = value;
        }

        public function get numLoadingTiles():int {
            return this._numLoadingTiles;
        }

        public function set numLoadingTiles(value:int):void {
            this._numLoadingTiles = value;
        }

        public function get xBuffer():int {
            return _xBuffer;
        }

        public function set xBuffer(value:int):void {
            _xBuffer = value;
        }

        public function get yBuffer():int {
            return _yBuffer;
        }

        public function set yBuffer(value:int):void {
            _yBuffer = value;
        }

        override public function clone():Layer {
            var grid:Grid = new Grid(this.id, this.name, this.url, this.params);
            return grid;
        }
    }
}

